
package jsr292.java.lang.invoke;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

public class MethodHandleProxies {
    static class ProxyHandler implements InvocationHandler {
        final Class<?> interfaze;
        final MethodHandle mh;
        private final Set<Method> methodSet;
        
        ProxyHandler(Class<?> interfaze, MethodHandle mh, Set<Method> methodSet) {
            this.interfaze = interfaze;
            this.mh = mh;
            this.methodSet = methodSet;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if (methodSet.contains(method)) {
                return mh.invokeWithArguments(args);
            }
            return invokeObjectMethod(proxy, method, args);
        }

        private static Object invokeObjectMethod(Object proxy, Method method, Object[] args) {
            switch(getObjectMethodMapping(method)) {
            case TO_STRING_INDEX:
                return proxy.getClass().getName() + '@' + Integer.toHexString(System.identityHashCode(proxy));
            case HASH_CODE_INDEX:
                return System.identityHashCode(proxy);
            case EQUALS_INDEX:
                return proxy == args[0];
            default: // NO_INDEX
            }
            throw new AssertionError(method);
        }
    }
    
    static final int NO_INDEX = -1;
    static final int TO_STRING_INDEX = 0;
    static final int HASH_CODE_INDEX = 1;
    static final int EQUALS_INDEX = 2;
    
    static int getObjectMethodMapping(Method method) {
        Class<?> returnType = method.getReturnType();
        Class<?>[] parameterTypes = method.getParameterTypes();
        String name = method.getName();
        switch(name.hashCode()) {
        case 0x9616526c:  // toString
            if (returnType != String.class || parameterTypes.length != 0 || !name.equals("toString")) {
                return -1;
            }
            return TO_STRING_INDEX;
        case 0x8cdac1b:  // hashCode
            if (returnType != int.class || parameterTypes.length != 0 || !name.equals("hashCode")) {
                return -1;
            }
            return HASH_CODE_INDEX;
        case 0xb2c87fbf:  // equals
            if (returnType != boolean.class || parameterTypes.length != 1 || !name.equals("equals")) {
                return -1;
            }
            return EQUALS_INDEX;
        }
        return NO_INDEX;
    }
    
    
    private MethodHandleProxies() {
        // no instance
    } 

    public static <T> T asInterfaceInstance(Class<T> interfaze, MethodHandle target) {
        if (!interfaze.isInterface()) {
            throw new IllegalArgumentException("not an interface " + interfaze);
        }
        Set<Method> methodSet = findSingleMethodSet(interfaze);
        if (methodSet.isEmpty()) {
            throw new IllegalArgumentException("not a single method interface " + interfaze);
        }
        return interfaze.cast(Proxy.newProxyInstance(interfaze.getClassLoader(),
                new Class<?>[]{ interfaze },
                new ProxyHandler(interfaze, target, methodSet)));
    }

    private static Set<Method> findSingleMethodSet(Class<?> interfaze) {
        String name = null;
        HashSet<Method> methodSet = new HashSet<Method>();
        for (Method method : interfaze.getMethods()) {
            if (!Modifier.isAbstract(method.getModifiers()) ||
                getObjectMethodMapping(method) != NO_INDEX) {
                continue;
            }
            
            String methodName = method.getName();
            if (name == null) {
                name = methodName;
                methodSet.add(method);
                continue;
            }
            if (name.equals(methodName)) {
                methodSet.add(method);
                continue;
            }
            return Collections.emptySet();
        }
        return methodSet;
    }

    public static boolean isWrapperInstance(Object o) {
        if (o == null) {
            return false;
        }
        return Proxy.getInvocationHandler(o) instanceof ProxyHandler;
    }

    private static ProxyHandler getProxyHandler(Object o) {
        if (o == null) {
            throw new IllegalArgumentException("reference is null");
        }
        InvocationHandler handler = Proxy.getInvocationHandler(o);
        if (!(handler instanceof ProxyHandler)) {
            throw new IllegalArgumentException("reference is not a wrapper instance");
        }
        return (ProxyHandler)handler;
    }

    public static MethodHandle wrapperInstanceTarget(Object o) {
        return getProxyHandler(o).mh;
    }

    public static Class<?> wrapperInstanceType(Object o) {
        return getProxyHandler(o).interfaze;
    }
}
